/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once

#include <Windows.h>

#include <baja/common.hpp>
#include <baja/error.hpp>
#include <baja/win32/details/util.hpp>

#include <baja/stream/stream_fwd.hpp>
#include <baja/stream/stream_types.hpp>
#include <baja/stream/stream_interfaces.hpp>

#include <baja/testable.hpp>

namespace w32d = ::baja::win32::details;

namespace baja { namespace win32 {

class file_stream : public istream, public testable_base
{
private:
    static const UINT invalid_pos = 0xffffffff;
    
public:
    file_stream() : _mode(open_mode::read) {}

    static std::shared_ptr<istream> create(const wchar* fileName, const open_mode mode)
    {
        std::shared_ptr<file_stream> fileStream = std::make_shared<file_stream>();

        fileStream->open(fileName, mode);

        return std::dynamic_pointer_cast<istream>(fileStream);
    } 
    
    size_t read(void* buffer, const size_t bytesToRead)
    {
        win32_result wr;

        if (!_fileHandle.isValid())
        {
            wr = E_FAIL;
        }

        if (bytesToRead > MAXDWORD32)
        {
            wr = E_FAIL;
        }

        DWORD numBytesRead = 0;

        wr = !!ReadFile(
            _fileHandle.get(), 
            buffer, 
            static_cast<DWORD>(bytesToRead), 
            &numBytesRead, 
            NULL
            );

        return static_cast<size_t>(numBytesRead);
    }

    size_t write(const void* buffer, const size_t bytesToWrite)
    {
        win32_result wr;

        DWORD myNumBytesWritten = 0;

        if (!_fileHandle.isValid())
        {
            wr = E_FAIL;
        }

        if (bytesToWrite > MAXDWORD32)
        {
            wr = E_FAIL;
        }

        ULONG cbWritten = -1;
        
        wr = !!WriteFile(
            _fileHandle.get(), 
            buffer, 
            static_cast<DWORD>(bytesToWrite), 
            &cbWritten, 
            NULL
            );

        return static_cast<size_t>(cbWritten);
    }
    
    size_t seek(const long long offset, const stream_origin origin)
    {
#ifdef BAJA_WINRT
        BAJA_THROW(std::logic_error("not supported in win32"));
        return 0;
#else
        win32_result wr;
        UINT moveMethod = FILE_BEGIN;
        UINT myNewPos = 0;

        if (!_fileHandle.isValid())
        {
            wr = E_FAIL;
        }

        // determine move method
        switch (origin)
        {
        case stream_origin::begin:
            moveMethod = FILE_BEGIN;
            break;

        case stream_origin::current:
            moveMethod = FILE_CURRENT;
            break;

        case stream_origin::end:
            moveMethod = FILE_END;
            break;
        
        default:
            wr = E_INVALIDARG;
            break;
        }

        // Perform the seek
        myNewPos = SetFilePointer(
            _fileHandle.get(), 
            offset, 
            NULL, 
            moveMethod
            );

        wr = (myNewPos != invalid_pos);
        
        return myNewPos;
#endif
    }

    void setSize(const size_t size)
    {
#ifdef BAJA_WINRT
        BAJA_THROW(std::logic_error("not supported in win32"));
#else
        win32_result wr;

        if (!_fileHandle.isValid())
        {
            wr = E_FAIL;
        }

        UINT myNewSize = static_cast<UINT>(size);
        
        // get the current file position
        UINT oldPos = SetFilePointer(
            _fileHandle.get(), 
            0, NULL, 
            FILE_CURRENT
            );

        // Assume -1 actually means failure rather than
        // "the current position is at 0xffffffff bytes into the
        // file" -- this may need to be revisited
        wr = (oldPos != invalid_pos);

        // Move to the desired end-of-file position
        UINT endPos = SetFilePointer(
            _fileHandle.get(),
            myNewSize, NULL, 
            FILE_BEGIN
            );

        wr = (endPos == myNewSize);

        // Set the new end-of-file marker
        wr = !!SetEndOfFile(_fileHandle.get());

        if (oldPos != invalid_pos)
        {
            // Restore file position, if we successfully retrieved it
            (void)SetFilePointer(
                _fileHandle.get(),
                oldPos, NULL, 
                FILE_BEGIN
                );
        }
#endif
    }
    
    size_t getSize()
    {
        FILE_STANDARD_INFO fileInfo;
        std::memset(&fileInfo, 0, sizeof(FILE_STANDARD_INFO));

        GetFileInformationByHandleEx(
            _fileHandle.get(),
            FILE_INFO_BY_HANDLE_CLASS::FileStandardInfo,
            &fileInfo,
            sizeof(FILE_STANDARD_INFO)
            );

        if (fileInfo.EndOfFile.HighPart != 0) BAJA_THROW(std::logic_error("file is too large"));

        return static_cast<size_t>(fileInfo.EndOfFile.LowPart);
    }

    void copyTo(const std::shared_ptr<istream>& s, const size_t cb, size_t* cbRead, size_t* cbWritten)
    {
        win32_result wr = E_NOTIMPL;
    }

    void commit(const uint32 flags)
    {
        win32_result wr = !!FlushFileBuffers(_fileHandle.get());
    }

    void revert() 
    {
        win32_result wr = E_NOTIMPL;
    }
    
    void lockRegion(const size_t offset, const size_t cb, const uint32 lockType)
    {
        win32_result wr = E_NOTIMPL;
    }

    void unlockRegion(const size_t offset, const size_t cb, const uint32 lockType)
    {
        win32_result wr = E_NOTIMPL;
    }

private:

    void open(const wchar* fileName, const open_mode mode)
    {
        BAJA_CHECK_ARG(fileName != nullptr)

        win32_result wr;
        w32d::safe_handle fileHandle;

        switch (mode)
        {
        case open_mode::read:
            fileHandle = w32d::safe_handle(CreateFile2(
                fileName,
                GENERIC_READ,
                FILE_SHARE_READ, 
                OPEN_EXISTING, 
                NULL
                ));
            break;

        case open_mode::create:
            fileHandle = w32d::safe_handle(CreateFile2(
                fileName, 
                GENERIC_READ | GENERIC_WRITE, 
                0, 
                CREATE_ALWAYS, 
                NULL
                ));
            break;

        case open_mode::append:
#ifdef BAJA_WINRT
        BAJA_THROW(std::logic_error("not supported in win32"));
#else
            fileHandle = w32d::safe_handle(CreateFile2(
                fileName, 
                GENERIC_READ | GENERIC_WRITE, 
                0, 
                OPEN_ALWAYS, 
                NULL
                ));
            
            if (fileHandle.get() != INVALID_HANDLE_VALUE)
            {
                UINT newPos = SetFilePointer(
                    fileHandle.get(), 
                    0, NULL, 
                    FILE_END
                    );

                wr = (newPos != invalid_pos);
            }
#endif
            break;

        default:
            wr = E_NOTIMPL;
            break;
        }
        
        wr = fileHandle.isValid();

        // transfer ownership
        _fileHandle.transferFrom(fileHandle);
        _mode = mode;
    }

#ifdef BAJA_CONFIG_TEST_BUILD
    void* inspectInternal(const std::string& iid)
    {
        if (iid.compare("handle") == 0)
        {
            return &_fileHandle;
        }
        return testable_base::inspectInternal(iid);
    }
#endif // BAJA_CONFIG_TEST_BUILD

private:
    w32d::safe_handle _fileHandle;
    open_mode _mode;
};

}} // namespace baja.win32

namespace baja {

inline std::shared_ptr<istream> createFileStream(
    const wchar* fileName,
    const open_mode mode
    )
{
    return baja::win32::file_stream::create(fileName, mode);
}

} // namespace baja.stream
